Flutter 你想知道的Widget可视区域,相对位置,大小

半夜睡不着觉,把心情写成代码,只好到这里水一篇bug


image

FlutterCandies QQ群:181398081
说起来这些东西,其实是一个怨念,从一个issue开始。

NestedScrollView Issue NestedScrollView里面有2个Scroll Control,一个outer(header),一个是inner(body),当inner里面有PageView/TabBarView,并且每个page被缓存(AutomaticKeepAliveClientMixin or PageStorageKey)的,滑动inner会对全部的列表都有影响

之前通过key的方式来判断哪个一个列表是当前可视区域里面激活的,让NestedScrollView滑动只对它有影响,之前的解决方案

其实我一开始就想知道怎么知道一个widget是不是在可视区域,日夜苦读,终于找到个可行的方案来优美的解决这个问题。

文字图代码会比较多。建议准备好瓜子水。边看边吃。。

我找到的第一个API是getOffsetToReveal

 /// The optional `rect` parameter describes which area of that `target` object
  /// should be revealed in the viewport. If `rect` is null, the entire
  /// `target` [RenderObject] (as defined by its [RenderObject.paintBounds])
  /// will be revealed. If `rect` is provided it has to be given in the
  /// coordinate system of the `target` object.
  ///
  /// The `alignment` argument describes where the target should be positioned
  /// after applying the returned offset. If `alignment` is 0.0, the child must
  /// be positioned as close to the leading edge of the viewport as possible. If
  /// `alignment` is 1.0, the child must be positioned as close to the trailing
  /// edge of the viewport as possible. If `alignment` is 0.5, the child must be
  /// positioned as close to the center of the viewport as possible.
  ///
  /// The target might not be a direct child of this viewport but it must be a
  /// descendant of the viewport and there must not be any other
  /// [RenderAbstractViewport] objects between the target and this object.
  ///
  /// This method assumes that the content of the viewport moves linearly, i.e.
  /// when the offset of the viewport is changed by x then `target` also moves
  /// by x within the viewport.
  ///
  /// See also:
  ///
  ///  * [RevealedOffset], which describes the return value of this method.
  RevealedOffset getOffsetToReveal(RenderObject target, double alignment, {Rect rect});

简单说下,就是获得目标RenderOject跟Viewport的距离,下面是主要用法

        RenderAbstractViewport viewport =
                RenderAbstractViewport.of(renderObject);

            /// Distance between top edge of screen and MyWidget bottom edge
            var offsetToRevealLeading =
                viewport.getOffsetToReveal(renderObject, 0.0);

            /// Distance between bottom edge of screen and MyWidget top edge
            var offsetToRevealTrailingEdge =
                viewport.getOffsetToReveal(renderObject, 1.0);

demo地址See your widget demo, demo中展示了怎么判断一个ListView里面一个Widget是否进入可视区域的

这是一个新的发现,吓的我赶快在TabBarView里面试了一下。。结果。。。

image

这个方法能判断出每个Tab相对于自己PageView/TabBarView可视区域的相对位置。通过判断PageView/TabBarView的position.pixels 与offsetToRevealLeading是否相等,来判断当前激活的Tab,但是当有多个PageView/TabBarView的时候。你就搞不清楚到底是哪个算是激活的,因为你需要先判断父PageView/TabBarView是否激活,然后才是子PageView/TabBarView

image

因为暂时没发现有什么好的方法区分,只是先暂时放弃,如果你有好的idea,请告诉我,万分感谢

后来我又找到个一个API (localToGlobal)

/// Convert the given point from the local coordinate system for this box to
  /// the global coordinate system in logical pixels.
  ///
  /// If `ancestor` is non-null, this function converts the given point to the
  /// coordinate system of `ancestor` (which must be an ancestor of this render
  /// object) instead of to the global coordinate system.
  ///
  /// This method is implemented in terms of [getTransformTo].
  Offset localToGlobal(Offset point, { RenderObject ancestor }) {
    return MatrixUtils.transformPoint(getTransformTo(ancestor), point);
  }

大概的意思是。。可以算出目标跟指定对象(ancestor)的相对位置。。结果如下

image

你能看出来什么吗? 哇塞,跟我想的一样,完美,用一个图表示为

image

这看起来是一条路。。

现在我们回到最上面那个issue,想解决这个issue我们还将遇到以下问题:

1.我们需要知道什么时候TabBarView/PageView的Page改变了。

为此我再次使用了熟悉的好东西NotificationListener
我的Flutter Candies当中大量使用到它

    if (widget.keepOnlyOneInnerNestedScrollPositionActive) {
      ///get notifications and compute active one in _innerController.nestedPositions
      return NotificationListener<ScrollNotification>(
          onNotification: (ScrollNotification notification) {
            if (notification is ScrollEndNotification &&
                notification.metrics is PageMetrics &&
                notification.metrics.axis == Axis.horizontal) {
              final PageMetrics metrics = notification.metrics;
              var depth = notification.depth;
              final int currentPage = metrics.page.round();
              var page = _pageMetricsList[depth];
              //ComputeActivatedNestedPosition only when page changed
              if (page != currentPage) {
                print("Page changed ${currentPage}");
                _coordinator._innerController
                    ._computeActivatedNestedPosition(notification);
              }
              _pageMetricsList[depth] = currentPage;
            }
            return false;
          },
          child: child);

使用NotificationListener监听PageMetrics,并且在Page changed时候通知去计算当前在可视区域的NestedPosition.

2.只用localToGlobal 这个玩意就足够了吗??

答案是不够的,因为ScrollEndNotification的时机还是不足够精确,导致会出现0.4,0.9之类的误差。。


image

解决方法:

1.加了一个100 milliseconds的延迟来执行计算

2.最后在结算与0的相比的值的时候做了个误差计算(因为不同Page的差至少为一个屏幕的差距,所以1的误差是可以忍受的)


image
void _computeActivatedNestedPosition(ScrollNotification notification,
      {Duration delay: const Duration(milliseconds: 100)}) {
    ///if layout is not completed, the data will has some gap.
    ///need more accurate time to compute
    ///delay it in case.
    ///to do
    Future.delayed(delay, () {
      /// this is the page changed of PageView's renderBox,
      /// it maybe not the renderBox of [nestedPositions]
      /// because it maybe has more one tabbarview or pageview in NestedScrollView body
      final RenderBox pageChangedRenderBox =
          notification.context.findRenderObject();
      int activeCount = 0;
      nestedPositions.forEach((item) {
        item._computeActived(pageChangedRenderBox);
        if (item._isActived) activeCount++;
      });

      if (activeCount > 1) {
        print(
            "activeCount more than 1, please report to zmtzawqlp@live.com and show your case.");
      }

      coordinator.updateCanDrag();
    });
  }

3.你以为这样就可以搞定了吗?

错了,我们忘记考虑padding和margin.


image

比如我给TabBarView的每个页面的List加了个一个PaddingEdgeInsets.only(left: 190.0),,让我们看看会有什么效果。

image

那我们怎么处理这个问题呢?从原因上面看通过_NestedScrollPosition的context得到的RenderBox只是这个List的RenderBox的区域,它跟PageView/TabBarView的RenderBox的相对位置不一定总会存在offset.x为0的状况,就像上面加了padding和margin一样

解决方式如下:
position 是List跟PageView/TabBarView的相对位置
size 是List跟PageView/TabBarView 大小的差距

通过这样的计算就能抵消padding和margin的影响,当然我这里没有再考虑transform这种东西了。。放过我吧。。

顺手送个Size的获取方式,RenderBox 有个Size属性

  final Offset position = child.localToGlobal(Offset.zero, ancestor: parent);
    ///remove the margin/padding
    final Offset size = Offset(parentSize.width - child.size.width,
        parentSize.height - child.size.height);

    ///if layout is not completed, the data will has some gap.
    ///need more accurate time to compute
    ///to do
    bool childIsActivedInViewport = ((position.dx - size.dx).abs() < 1 &&
        (position.dy - size.dy).abs() < 1);

4.完美,perfect,beautiful??

忘记考虑多个TabBarView/PageView对结果的影响


image
image

为啥会出现这种情况呢? 因为开始我是使用的从ScrollEndNotification的Context计算出来的RenderBox,注意这个是不管你是哪个TabBarView/PageView的Page发生变化的,

但是其实上,比如Tab0切换到Tab1的时候。你应该关心的是Tab1 下面的Tab10,Tab11,Tab12,Tab13的状态,Tab0下面应该都是不激活的.

其实我们应该还要找到_NestedScrollPosition所对应的PageView/TabBarView,计算_NestedScrollPosition和PageView/TabBarView的相对位置。

所以判断_NestedScrollPosition是否为当前可视区域的激活的条件应该如下:

1.ScrollEndNotification的RenderBox和_NestedScrollPosition的RenderBox的相对位置符合

2._NestedScrollPosition对应的PageView/TabBarView的RenderBox跟_NestedScrollPosition的RenderBox的相对位置符合

打印结果也证明了这点:


image

5.结束了??

没有,localToGlobal这个方法,在一种情况下会报错。

image

进入localToGlobal中,再进去getTransformTo

 Matrix4 getTransformTo(RenderObject ancestor) {
    assert(attached);
    if (ancestor == null) {
      final AbstractNode rootNode = owner.rootNode;
      if (rootNode is RenderObject)
        ancestor = rootNode;
    }
    final List<RenderObject> renderers = <RenderObject>[];
    for (RenderObject renderer = this; renderer != ancestor; renderer = renderer.parent) {
      assert(renderer != null); // Failed to find ancestor in parent chain.
      renderers.add(renderer);
    }
    final Matrix4 transform = Matrix4.identity();
    for (int index = renderers.length - 1; index > 0; index -= 1)
      renderers[index].applyPaintTransform(renderers[index - 1], transform);
    return transform;
  }

这里可能会触发

assert(renderer != null); // Failed to find ancestor in parent

分析:说明你提供的ancestor 跟_NestedScrollPosition 没有关联,这时候我们直接try catch, 设置为不激活状态就好了。。

6.应该可以睡觉了吧

可以,但是我还想说2点。

1.如果当计算之后,有超过2个的nestedPositions,请告诉我一下,看看你那个复杂的case是啥(实际上,demo里面栗子已经是很复杂的了)

 int activeCount = 0;
    nestedPositions.forEach((item) {
      item._computeActived(pageChangedRenderBox);
      if (item._isActived) activeCount++;
    });

    if (activeCount > 1) {
      print(
          "activeCount more than 1, please report to zmtzawqlp@live.com and show your case.");
    }

2.extended_nested_scroll_view

我只考虑了NestedScrollView滚动方向是垂直而且PageView/TabBarView是水平滚动的情况.

如果你有啥子妖魔鬼怪的布局,你可以试试老的extended_nested_scroll_view

最后放上 Github extended_nested_scroll_view,如果你有什么不明白的地方,请告诉我。

image
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 160,165评论 4 364
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,720评论 1 298
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 109,849评论 0 244
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,245评论 0 213
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,596评论 3 288
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,747评论 1 222
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,977评论 2 315
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,708评论 0 204
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,448评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,657评论 2 249
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,141评论 1 261
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,493评论 3 258
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,153评论 3 238
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,108评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,890评论 0 198
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,799评论 2 277
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,685评论 2 272

推荐阅读更多精彩内容